{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Keras MNIST Model Deployment\n",
    "\n",
    " * Wrap a Tensorflow MNIST python model for use as a prediction microservice in seldon-core\n",
    "   * Run locally on Docker to test\n",
    "   * Deploy on seldon-core running on minikube\n",
    " \n",
    "## Depenencies\n",
    "\n",
    " * [Helm](https://github.com/kubernetes/helm)\n",
    " * [Minikube](https://github.com/kubernetes/minikube)\n",
    " * [S2I](https://github.com/openshift/source-to-image)\n",
    "\n",
    "```bash\n",
    "pip install keras\n",
    "pip install grpcio-tools\n",
    "```\n",
    "\n",
    "## Train locally\n",
    " "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import math\n",
    "import datetime\n",
    "#from seldon.pipeline import PipelineSaver\n",
    "import os\n",
    "import tensorflow as tf\n",
    "from keras import backend\n",
    "from keras.models import Model,load_model\n",
    "from keras.layers import Dense,Input\n",
    "from keras.layers import Dropout\n",
    "from keras.layers import Flatten, Reshape\n",
    "from keras.constraints import maxnorm\n",
    "from keras.layers.convolutional import Convolution2D\n",
    "from keras.layers.convolutional import MaxPooling2D\n",
    "\n",
    "from keras.callbacks import TensorBoard\n",
    "\n",
    "class MnistFfnn(object):\n",
    "\n",
    "    def __init__(self,\n",
    "                 input_shape=(784,),\n",
    "                 nb_labels=10,\n",
    "                 optimizer='Adam',\n",
    "                 run_dir='tensorboardlogs_test'):\n",
    "        \n",
    "        self.model_name='MnistFfnn'\n",
    "        self.run_dir=run_dir\n",
    "        self.input_shape=input_shape\n",
    "        self.nb_labels=nb_labels\n",
    "        self.optimizer=optimizer\n",
    "        self.build_graph()\n",
    "\n",
    "    def build_graph(self):\n",
    "                            \n",
    "        inp = Input(shape=self.input_shape,name='input_part')\n",
    "\n",
    "        #keras layers\n",
    "        with tf.name_scope('dense_1') as scope:\n",
    "            h1 = Dense(256,\n",
    "                         activation='relu',\n",
    "                         W_constraint=maxnorm(3))(inp)\n",
    "            drop1 = Dropout(0.2)(h1)\n",
    "\n",
    "        with tf.name_scope('dense_2') as scope:\n",
    "            h2 = Dense(128,\n",
    "                       activation='relu',\n",
    "                       W_constraint=maxnorm(3))(drop1)\n",
    "            drop2 = Dropout(0.5)(h2)\n",
    "            \n",
    "            out = Dense(self.nb_labels,\n",
    "                        activation='softmax')(drop2)\n",
    "\n",
    "        self.model = Model(inp,out)\n",
    "        \n",
    "        if self.optimizer ==  'rmsprop':\n",
    "            self.model.compile(loss='categorical_crossentropy',\n",
    "                               optimizer='rmsprop',\n",
    "                               metrics=['accuracy'])\n",
    "        elif self.optimizer == 'Adam':\n",
    "            self.model.compile(loss='categorical_crossentropy',\n",
    "                               optimizer='Adam',\n",
    "                               metrics=['accuracy'])\n",
    "            \n",
    "        print('graph builded')\n",
    "\n",
    "    def fit(self,X,y=None,\n",
    "            X_test=None,y_test=None,\n",
    "            batch_size=128,\n",
    "            nb_epochs=2,\n",
    "            shuffle=True):\n",
    "        \n",
    "        now = datetime.datetime.now()\n",
    "        tensorboard_logname = self.run_dir+'/{}_{}'.format(self.model_name,\n",
    "                                                           now.strftime('%Y.%m.%d_%H.%M'))      \n",
    "        tensorboard = TensorBoard(log_dir=tensorboard_logname)\n",
    "        \n",
    "        self.model.fit(X,y,\n",
    "                       validation_data=(X_test,y_test),\n",
    "                       callbacks=[tensorboard],\n",
    "                       batch_size=batch_size, \n",
    "                       nb_epoch=nb_epochs,\n",
    "                       shuffle = shuffle)\n",
    "        return self\n",
    "    \n",
    "    def predict_proba(self,X):\n",
    "\n",
    "        return self.model.predict_proba(X)\n",
    "    \n",
    "    def predict(self, X):\n",
    "        probas = self.model.predict_proba(X)\n",
    "        return([[p>0.5 for p in p1] for p1 in probas])\n",
    "        \n",
    "    def score(self, X, y=None):\n",
    "        pass\n",
    "\n",
    "    def get_class_id_map(self):\n",
    "        return [\"proba\"]\n",
    "\n",
    "class MnistConv(object):\n",
    "\n",
    "    def __init__(self,\n",
    "                 input_shape=(784,),\n",
    "                 nb_labels=10,\n",
    "                 optimizer='Adam',\n",
    "                 run_dir='tensorboardlogs_test',\n",
    "                 saved_model_file='MnistClassifier.h5'):\n",
    "        \n",
    "        self.model_name='MnistConv'\n",
    "        self.run_dir=run_dir\n",
    "        self.input_shape=input_shape\n",
    "        self.nb_labels=nb_labels\n",
    "        self.optimizer=optimizer\n",
    "        self.saved_model_file=saved_model_file\n",
    "        self.build_graph()\n",
    "\n",
    "    def build_graph(self):\n",
    "                                                                \n",
    "        inp = Input(shape=self.input_shape,name='input_part')\n",
    "        inp2 = Reshape((28,28,1))(inp)      \n",
    "        #keras layers\n",
    "        with tf.name_scope('conv') as scope:\n",
    "            conv = Convolution2D(32, 3, 3,\n",
    "                                 input_shape=(32, 32, 3),\n",
    "                                 border_mode='same',\n",
    "                                 activation='relu',\n",
    "                                 W_constraint=maxnorm(3))(inp2)\n",
    "            drop_conv = Dropout(0.2)(conv)\n",
    "            max_pool = MaxPooling2D(pool_size=(2, 2))(drop_conv)\n",
    "\n",
    "        with tf.name_scope('dense') as scope:\n",
    "            flat = Flatten()(max_pool)                \n",
    "            dense = Dense(128,\n",
    "                          activation='relu',\n",
    "                          W_constraint=maxnorm(3))(flat)\n",
    "            drop_dense = Dropout(0.5)(dense)\n",
    "            \n",
    "            out = Dense(self.nb_labels,\n",
    "                        activation='softmax')(drop_dense)\n",
    "\n",
    "        self.model = Model(inp,out)\n",
    "        \n",
    "        if self.optimizer ==  'rmsprop':\n",
    "            self.model.compile(loss='categorical_crossentropy',\n",
    "                               optimizer='rmsprop',\n",
    "                               metrics=['accuracy'])\n",
    "        elif self.optimizer == 'Adam':\n",
    "            self.model.compile(loss='categorical_crossentropy',\n",
    "                               optimizer='Adam',\n",
    "                               metrics=['accuracy'])\n",
    "            \n",
    "        print('graph builded')\n",
    "\n",
    "    def fit(self,X,y=None,\n",
    "            X_test=None,y_test=None,\n",
    "            batch_size=128,\n",
    "            nb_epochs=2,\n",
    "            shuffle=True):\n",
    "        \n",
    "        now = datetime.datetime.now()\n",
    "        tensorboard_logname = self.run_dir+'/{}_{}'.format(self.model_name,\n",
    "                                                           now.strftime('%Y.%m.%d_%H.%M'))      \n",
    "        tensorboard = TensorBoard(log_dir=tensorboard_logname)\n",
    "        \n",
    "        self.model.fit(X,y,\n",
    "                       validation_data=(X_test,y_test),\n",
    "                       callbacks=[tensorboard],\n",
    "                       batch_size=batch_size, \n",
    "                       nb_epoch=nb_epochs,\n",
    "                       shuffle = shuffle)\n",
    "        #if not os.path.exists('saved_model'):\n",
    "        #    os.makedirs('saved_model')\n",
    "        self.model.save(self.saved_model_file)\n",
    "        return self\n",
    "    \n",
    "    def predict_proba(self,X):\n",
    "        return self.model.predict_proba(X)\n",
    "    \n",
    "    def predict(self, X):\n",
    "        probas = self.model.predict_proba(X)\n",
    "        return([[p>0.5 for p in p1] for p1 in probas])\n",
    "        \n",
    "    def score(self, X, y=None):\n",
    "        pass\n",
    "\n",
    "    def get_class_id_map(self):\n",
    "        return [\"proba\"]\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from tensorflow.examples.tutorials.mnist import input_data\n",
    "mnist = input_data.read_data_sets('data/MNIST_data', one_hot=True)\n",
    "X_train = mnist.train.images\n",
    "y_train = mnist.train.labels\n",
    "X_test = mnist.test.images\n",
    "y_test = mnist.test.labels\n",
    "mc = MnistConv()\n",
    "mc.fit(X_train,y=y_train,\n",
    "    X_test=X_test,y_test=y_test)\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Wrap model using s2i"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!s2i build . seldonio/seldon-core-s2i-python3:0.2 keras-mnist:0.1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!docker run --name \"mnist_predictor\" -d --rm -p 5000:5000 keras-mnist:0.1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!cd ../../../wrappers/testing && make build_protos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Send some random features that conform to the contract"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!python ../../../wrappers/testing/tester.py contract.json 0.0.0.0 5000 -p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!docker rm mnist_predictor --force"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Test using Minikube\n",
    "\n",
    "**Due to a [minikube/s2i issue](https://github.com/SeldonIO/seldon-core/issues/253) you will need Minikube version 0.25.2**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!minikube start --memory 4096 --feature-gates=CustomResourceValidation=true --extra-config=apiserver.Authorization.Mode=RBAC"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!kubectl create clusterrolebinding kube-system-cluster-admin --clusterrole=cluster-admin --serviceaccount=kube-system:default"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!helm init"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!kubectl rollout status deploy/tiller-deploy -n kube-system"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!helm install ../../../helm-charts/seldon-core-crd --name seldon-core-crd  --set usage_metrics.enabled=true\n",
    "!helm install ../../../helm-charts/seldon-core --name seldon-core "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!eval $(minikube docker-env) && s2i build . seldonio/seldon-core-s2i-python3:0.2 keras-mnist:0.1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!kubectl create -f keras_mnist_deployment.json"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Wait until ready (replicas == replicasAvailable)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!kubectl get seldondeployments seldon-deployment-example -o jsonpath='{.status}' "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!cd ../../../util/api_tester && make build_protos "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!python ../../../util/api_tester/api-tester.py contract.json \\\n",
    "    `minikube ip` `kubectl get svc -l app=seldon-apiserver-container-app -o jsonpath='{.items[0].spec.ports[0].nodePort}'` \\\n",
    "    --oauth-key oauth-key --oauth-secret oauth-secret -p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!minikube delete"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
